/*
 * Copyright 2010 WANdisco, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License. 
 */
package com.googlecode.svnj.dav;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;

import org.apache.jackrabbit.util.Text;
import org.apache.jackrabbit.webdav.DavCompliance;
import org.apache.jackrabbit.webdav.DavConstants;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavResourceIterator;
import org.apache.jackrabbit.webdav.DavResourceIteratorImpl;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.MultiStatusResponse;
import org.apache.jackrabbit.webdav.io.InputContext;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.ActiveLock;
import org.apache.jackrabbit.webdav.lock.LockInfo;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.lock.Scope;
import org.apache.jackrabbit.webdav.lock.Type;
import org.apache.jackrabbit.webdav.property.DavProperty;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DavPropertySet;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.apache.jackrabbit.webdav.property.ResourceType;
import org.apache.jackrabbit.webdav.version.DeltaVConstants;
import org.apache.jackrabbit.webdav.version.DeltaVResource;
import org.apache.jackrabbit.webdav.version.OptionsInfo;
import org.apache.jackrabbit.webdav.version.OptionsResponse;
import org.apache.jackrabbit.webdav.version.report.Report;
import org.apache.jackrabbit.webdav.version.report.ReportInfo;
import org.apache.jackrabbit.webdav.xml.DomUtil;
import org.apache.jackrabbit.webdav.xml.Namespace;
import org.apache.jackrabbit.webdav.xml.XmlSerializable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.googlecode.svnj.RepoContext;
import com.googlecode.svnj.XMLUtil;
import com.googlecode.svnj.RepoContext.DirEntry;
import com.googlecode.svnj.reports.Depth;

public class SVNResource implements DavResource, DeltaVResource, SVNConstants {

    private static final Logger log = LoggerFactory.getLogger(SVNResource.class);
    
    private static final String OPTIONS_METHODS = 
        "OPTIONS,GET,HEAD,POST,DELETE,TRACE,PROPFIND,PROPPATCH,COPY,MOVE,LOCK,UNLOCK,CHECKOUT";
    private DavResourceLocator locator;
    private RepoContext repo;
    private RepoContext.DirEntry entry;
    private SpecialResource res;
    // relative path from root of repo
    private String relpath;
    /* part of the path after '!' if any...e.g.,
     * /svn/alphabet/!svn/bln/8
     */
    private static void nyi(String method) {
        String msg = "NYI - '" + method + "'";
        log.error(msg);
        throw new RuntimeException(msg);
    }
    public SVNResource(RepoContext repo, String relUri, DavResourceLocator locator) {
        this.repo = repo;
        String fullUri = Path.join(repo.getContextPath(), relUri);
        res = new SpecialResource(repo.getContextPath(), fullUri, repo.getRootRev());
        // remount the repository if the resource specifies a particular rev,
        // and we're not already mounted at that rev
        if (res.getRev() != -1 && res.getRev() != repo.getRootRev()) {
            repo.remount(res.getRev());
        }
        //this.relpath = relpath;
        //this.relpath = res.getRelUri();
        this.relpath = res.getReposPath();
        
        if (this.relpath != null && this.relpath.startsWith("/")) {
            this.relpath = this.relpath.substring(1);
        }
        this.locator = locator;
        this.entry = repo.getEntry(relUri);
    }
    
    /** DeltaVResource methods */
    public void addWorkspace(DavResource workspace) throws DavException {
        nyi("addWorkspace");
    }

    public OptionsResponse getOptionResponse(OptionsInfo optionsInfo) {
        OptionsResponse or = null;
        if (optionsInfo != null) {
            or = new OptionsResponse();
            // currently only DAV:activity-collection-set is supported
            if (optionsInfo.containsElement(DeltaVConstants.XML_ACTIVITY_COLLECTION_SET, DeltaVConstants.NAMESPACE)) {
                String[] hrefs = new String[] {
                        Path.join(repo.getContextPath(), "/!svn/act/")
                };
                or.addEntry(DeltaVConstants.XML_ACTIVITY_COLLECTION_SET, DeltaVConstants.NAMESPACE, hrefs);
            }
        }
        return or;        
    }

    public DavResource[] getReferenceResources(DavPropertyName hrefPropertyName)
            throws DavException {
        nyi("getReferenceResources");
        return null;
    }

    public Report getReport(ReportInfo reportInfo) throws DavException {
        nyi("getReport");
        return null;
    }

    /** DavResource methods */
    public void addLockManager(LockManager lockmgr) {
        nyi("addLockManager");
    }

    public void addMember(DavResource resource, InputContext inputContext)
            throws DavException {
        if (!this.isCollection()) {
            // FIXME: not sure this is right, guessing b/c it makes sense
            String msg = "cannot add member to '" + resource.getHref() + "' unless the resource is a collection";
            log.error(msg);
            throw new RuntimeException(msg);
        }
        nyi("addMember");
    }

    public MultiStatusResponse alterProperties(List changeList)
            throws DavException {
        nyi("alterproperties");
        return null;
    }

    public void copy(DavResource destination, boolean shallow)
            throws DavException {
        nyi("copy");
    }

    public boolean exists() {
        //nyi("exists");
        return true;
    }
    
    public DavResource getCollection() {
        // get the parent resource
        //return IO.get().makeResource(file.getParentFile());
        nyi("getCollection");
        return null;
    }
    
    public String getComplianceClass() {
        return DavCompliance.concatComplianceClasses(
                new String[] {
                    DavCompliance._1_,
                    DavCompliance._2_,
                    //DavCompliance._3_,
                    //DavCompliance.BIND
                }
                );
    }
    
    public String getDisplayName() {
        String resPath = getResourcePath();
        return (resPath != null) ? Text.getName(resPath) : resPath;
    }
    
    public DavResourceFactory getFactory() {
        nyi("getFactory");
        return null;
    }

    public SpecialResource getSpecialResource() { return res; }
    public String getHref() {
        /*
        String href = getLocator().getHref(isCollection());
        return href;
        String ret = Path.join(repo.getContextPath(), relpath);
        */
        String ret = this.res.getHref();
        if (isCollection()) {
            ret = Path.join(ret, ""); // ensure ends with '/'
        }
        /*FIXME if (specialPath != null) {
            ret = Path.join(ret, '!' + specialPath);
        }*/
        return ret;
    }

    public DavResourceLocator getLocator() {
        return locator;
    }
    
    public ActiveLock getLock(Type type, Scope scope) {
        return IO.get().getLockManager().getLock(type, scope, this);
    }
    
    public ActiveLock[] getLocks() {
        ActiveLock ret = IO.get().getLockManager().getLock(Type.WRITE, Scope.EXCLUSIVE, this);
        return ret != null ? new ActiveLock[] { ret } : null; 
    }
    
    public DavResourceIterator getMembers() {
        DavResourceIterator ret = null;
        if (isCollection()) {
            List<DavResource> l = new ArrayList();
            DirEntry[] entries = repo.list(relpath);
            for (DirEntry e : entries) {
                String name = e.getName();
                String path = Path.join(this.relpath, name);
                SVNResource res = IO.get().makeResource(repo, path);
 //               p("href for " + f.getAbsolutePath() + " is '" + res.getHref() + "'");
                l.add(res);
            }
            ret = new DavResourceIteratorImpl(l);
        }
        return ret;
    }

    public long getModificationTime() {
        //return file.lastModified();
        nyi("getModTime");
        return -1;
    }

    // FIXME: date format not threadsafe...
    // http://solidsimplesafe.com/view/13
    protected static final SimpleDateFormat CREATION_DATE_FORMAT = new SimpleDateFormat(
            "yyyy-MM-dd'T'HH:mm:ss'Z'");

    /**
     * Simple date format for the last modified date. (RFC 822 updated by RFC
     * 1123)
     */
    // FIXME: date format not threadsafe...
    // http://solidsimplesafe.com/view/13
    protected static final SimpleDateFormat LAST_MODIFIED_DATE_FORMAT = new SimpleDateFormat(
            "EEE, dd MMM yyyy HH:mm:ss z", Locale.US);

    static {
        CREATION_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
        LAST_MODIFIED_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
    }
    
    protected String getETag() {
        // same (simplistic?) etag generation scheme as mod_dav_svn
        if (res.getReposPath() == null) {
            return null;
        }
        long createdRev = repo.getCreatedRev(relpath);
        String ret;
        if (isCollection()) {
            ret = "W/\"" + createdRev + '/' + relpath + '"'; 
        } else {
            ret = "\"" + createdRev + '/' + relpath + '"';
        }
        return ret;
    }
    private DavProperty makeProp(Namespace ns, String name, Object value) {
        DavPropertyName dn = DavPropertyName.create(name, ns);
        return makeProp(dn, value);
    }
    private DavProperty makeProp(DavPropertyName dname, Object value) {
        DavProperty ret = new DefaultDavProperty(dname, value, false);
        return ret;
    }
    private static class HrefValue implements XmlSerializable {

        private String value;
        HrefValue(String s) { value = s; }
        public Element toXml(Document doc) {
            Element elem = DomUtil.createElement(doc, "href", DavConstants.NAMESPACE, value);
            return elem;
        }
    }
    private String getLastChangeAuthor(String path) {
        // FIXME
        String ret = null;
        long rev = repo.getCreatedRev(path);
        if (rev != -1) {
            Map<String,String> map = repo.getRevProperties(rev);
            ret = map.get("svn:author");
        }
        return ret;
    }
    public DavPropertySet getProperties() {
        //nyi("getProperties");
        DavPropertySet pset = new DavPropertySet();
        pset.add(makeProp(DavPropertyName.GETETAG, getETag()));
        //pset.add(makeProp(DavPropertyName.CREATIONDATE, CREATION_DATE_FORMAT.format(new Date(file.lastModified()))));
        pset.add(makeProp(DavPropertyName.DISPLAYNAME, this.getDisplayName()));
        pset.add(makeProp(NS_SVN_DAV, SVNConstants.UUID, repo.getUuid()));
        // handle creator-displayname 
        {
            String creator = null;
            if (res.getReposPath() != null) {
                creator = getLastChangeAuthor(res.getReposPath());
            }
            pset.add(makeProp(DeltaVConstants.CREATOR_DISPLAYNAME, creator));
        }
        if (res.getType() != SpecialResource.Type.REV_COLLECTION) {
            pset.add(makeProp(DavConstants.NAMESPACE, SVNConstants.CI,
                    new HrefValue(repo.getContextPath() + "/!svn/bln/" + repo.getYoungestRev())));
        
        
            pset.add(makeProp(NS_SVN_DAV, SVNConstants.BASELINE_RELATIVE_PATH, "/".equals(relpath) ? null: relpath));

            pset.add(makeProp(DavConstants.NAMESPACE, SVNConstants.VCC,
                    new HrefValue(Path.join(repo.getContextPath(), "!svn/vcc/default"))));
        }
        pset.add(makeProp(DavConstants.NAMESPACE, SVNConstants.BC,
                new HrefValue(
                        Path.join(repo.getContextPath(), "!svn/bc/" + (repo.getRootRev()) + "/")
                        )));
        long version = repo.getRootRev();
        if (res.getReposPath() != null)
            version = repo.getCreatedRev(res.getReposPath());
        pset.add(makeProp(DavConstants.NAMESPACE, SVNConstants.VN, String.valueOf(version)));
        //pset.add(makeProp(DavPropertyName.GETLASTMODIFIED, LAST_MODIFIED_DATE_FORMAT.format(new Date(file.lastModified()))));
        
        // resource type
        if (res.getType() == SpecialResource.Type.REV_COLLECTION) {
            pset.add(new ResourceType(ResourceType.BASELINE));
        } else if (this.isCollection()) {
            pset.add(new ResourceType(ResourceType.COLLECTION));
            pset.add(makeProp(SVNConstants.NS_SVN_DAV, SVNConstants.MD5, null));
            // windows
            //pset.add(makeProp(DavPropertyName.RESOURCETYPE, "1"));
        } else {
            pset.add(new ResourceType(ResourceType.DEFAULT_RESOURCE));
            // windows
            //pset.add(makeProp(DavPropertyName.RESOURCETYPE, "0"));
            pset.add(makeProp(DavPropertyName.GETCONTENTLENGTH, String.valueOf(entry.getContentLength())));
            pset.add(makeProp(DavPropertyName.RESOURCETYPE, null));
            String md5 = null;
            if (res.getType() == SpecialResource.Type.NOT_SPECIAL) {
                md5 = repo.md5(relpath);
            }
            pset.add(makeProp(SVNConstants.NS_SVN_DAV, SVNConstants.MD5, md5));
        }
        // add svn versioned 'deadprops', iff a real resource
        if (res.getReposPath() != null) {
            Map<String,String> svnProps = repo.getPathProperties(res.getReposPath(), Depth.IMMEDIATES);
            pset.add(makeProp(NS_SVN_DAV, SVNConstants.PROP_DEADPROP_COUNT, String.valueOf(svnProps.size())));
            addMapAsProps(pset, svnProps);
            String pval;
            if ((pval = svnProps.get("content-type")) != null) {
                pset.add(makeProp(DavPropertyName.GETCONTENTTYPE, pval));
            }
        } else if (res.getType() == SpecialResource.Type.REV_COLLECTION) {
            // add rev props
            Map<String,String> revProps = repo.getRevProperties(res.getRev());
            addMapAsProps(pset, revProps);
        }
        
        return pset;
    }

    private void addMapAsProps(DavPropertySet pset, Map<String,String> map) {
        for (String pname : map.keySet()) {
            /* FIXME: escape unsafe xml here */
            String pval = map.get(pname);
            Namespace ns = SVNConstants.NS_CUSTOM;
            int ind;
            if (pname.startsWith("svn:")) {
                pname = pname.substring(4);
                ns = SVNConstants.NS_SVN;
            } else if ((ind = pname.indexOf(':')) != -1) {
                pname = pname.substring(ind + 1);
            }
            log.info("adding prop '" + pname + "'='" + pval + "'");
            if (pval != null && pval.length() == 0) pval = null;
            pset.add(makeProp(ns, pname, pval));
        }        
    }
    public DavProperty getProperty(DavPropertyName name) {
        // FIXME: horribly inefficent
        DavPropertySet pset = getProperties();
        return pset.get(name);
    }
    
    public DavPropertyName[] getPropertyNames() {
        DavPropertySet pset = getProperties();
        return pset.getPropertyNames();
    }
    
    public String getResourcePath() {
        return relpath;
    }
    
    public DavSession getSession() {
        nyi("getSession");
        return null;
    }
    
    public String getSupportedMethods() {
        return OPTIONS_METHODS;
    }
    
    public boolean hasLock(Type type, Scope scope) {
        nyi("hasLock");
        return false;
    }

    public boolean isCollection() {
        boolean ret = repo.isDirectory(relpath);
        return ret;
        //return file.isDirectory();
    }
    
    public boolean isLockable(Type type, Scope scope) {
        nyi("isLockable");
        return false;
    }
    
    public ActiveLock lock(LockInfo reqLockInfo) throws DavException {
        return IO.get().getLockManager().createLock(reqLockInfo, this);
    }
    
    public void move(DavResource destination) throws DavException {
        nyi("move");
    }
    
    public ActiveLock refreshLock(LockInfo reqLockInfo, String lockToken)
            throws DavException {
        return IO.get().getLockManager().refreshLock(reqLockInfo, lockToken, this);
    }
    
    public void removeMember(DavResource member) throws DavException {
        nyi("removeMember");
        /*
        if (!this.isCollection()) {
            // FIXME: not sure this is right, guessing b/c it makes sense
            throw new RuntimeException(
                    "cannot remove members of a non-collection");
        }
        File outFile = ((SVNResource)member).getFile();
        if (!outFile.delete()) {
            throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR,
                    "cannot delete '" + member.getResourcePath() + "'");
        }
        */
    }
    
    public void removeProperty(DavPropertyName propertyName)
            throws DavException {
        nyi("removeProperty");

    }
    
    public void setProperty(DavProperty property) throws DavException {
        nyi("setProperty");
    }
    
    public void spool(OutputContext outputContext) throws IOException {
        try {
            OutputStream os = outputContext.getOutputStream();
            if (this.isCollection()) {
                produceDirListing(os);
            } else {
                if (os != null) {
                    repo.writeContents(relpath, os);
                } else {
                    // s'okay...get this on a HEAD request
                    //throw new RuntimeException("null outputstream for " + outputContext);
                    // FIXME: set content-length, mod-time, etc...
                    //outputContext.s
                }
            }
        } catch (IOException ioe) {
            log.error("cannot spool " + getResourcePath(), ioe);
            throw ioe;
        }
    }
    
    private void produceDirListing(OutputStream os) throws IOException {
        PrintWriter writer = new PrintWriter(new OutputStreamWriter(os, "utf8"));
        /*
         * Item item = context.getExportRoot(); Repository rep =
         * item.getSession().getRepository(); String repName =
         * rep.getDescriptor(Repository.REP_NAME_DESC); String repURL =
         * rep.getDescriptor(Repository.REP_VENDOR_URL_DESC); String repVersion
         * = rep.getDescriptor(Repository.REP_VERSION_DESC);
         */
        writer.print("<html><head><title>");
        writer.print(this.getResourcePath());
        writer.print(" ");
        writer.print("</title></head>");
        writer.print("<body><h2>");
        writer.print(getResourcePath());
        writer.print("</h2><ul>");
        writer.print("<li><a href=\"..\">..</a></li>");
        DavResourceIterator iter = this.getMembers();
        while (iter.hasNext()) {
            DavResource child = iter.nextResource();
            String label = Text.getName(child.getDisplayName());
            String suffix = child.isCollection() ? "/" : "";
            writer.print("<li><a href=\"");
            writer.print(Text.escape(label));
            writer.print(suffix);
            writer.print("\">");
            writer.print(label);
            writer.print(suffix);
            writer.print("</a></li>");
        }
        writer.print("</ul><hr size=\"1\"><em><a href=\"http://code.google.com/p/svnj/\">Powered by SVNJ</a></em>");
        writer.print("</body></html>");
        writer.flush();
    }
    
    public void unlock(String lockToken) throws DavException {
        IO.get().getLockManager().releaseLock(lockToken, this);
    }
}
